enhance exif subsecond and offset handling. (#1033)
authortsteven4 <13596209+tsteven4@users.noreply.github.com>
Sat, 11 Mar 2023 18:14:29 +0000 (11:14 -0700)
committerGitHub <noreply@github.com>
Sat, 11 Mar 2023 18:14:29 +0000 (11:14 -0700)
* enhance exif subsecond and offset handling.

* don't warn for legal empty offset tags.

exif.cc
exif.h
reference/format3.txt
reference/help.txt
xmldoc/formats/options/exif-frame.xml
xmldoc/formats/options/exif-offset.xml [new file with mode: 0644]

diff --git a/exif.cc b/exif.cc
index 77fa91622cb25974ad242d055976fda949aa8bb8..da634db51aeb30a821ceba94544bb1824ef4baab 100644 (file)
--- a/exif.cc
+++ b/exif.cc
@@ -677,7 +677,7 @@ ExifFormat::exif_find_tag(ExifApp* app, const uint16_t ifd_nr, const uint16_t ta
 }
 
 QDateTime
-ExifFormat::exif_get_exif_time(ExifApp* app)
+ExifFormat::exif_get_exif_time(ExifApp* app) const
 {
   QDateTime res;
 
@@ -700,20 +700,24 @@ ExifFormat::exif_get_exif_time(ExifApp* app)
     // Exif 2.31 added offset tags to record the offset to UTC.
     // If these are present use them, otherwise assume local time.
     ExifTag* offset_tag = nullptr;
+    ExifTag* subsec_tag = nullptr;
     switch (tag->id) {
     case 0x9003:
       offset_tag = exif_find_tag(app, EXIF_IFD, 0x9011);  /* OffsetTimeOriginal from EXIF */
+      subsec_tag = exif_find_tag(app, EXIF_IFD, 0x9291);  /* SubSecTimeOriginal from EXIF */
       break;
     case 0x0132:
       offset_tag = exif_find_tag(app, EXIF_IFD, 0x9010);  /* OffsetTime from EXIF */
+      subsec_tag = exif_find_tag(app, EXIF_IFD, 0x9290);  /* SubSecTime from EXIF */
       break;
     case 0x9004:
       offset_tag = exif_find_tag(app, EXIF_IFD, 0x9012);  /* OffsetTimeDigitized from EXIF */
+      subsec_tag = exif_find_tag(app, EXIF_IFD, 0x9292);  /* SubSecTimeDigitized from EXIF */
       break;
     }
 
-    if (offset_tag) {
-      QByteArray time_tag = exif_read_str(offset_tag);
+    if (offset_tag || opt_offsettime) {
+      QByteArray time_tag = opt_offsettime? QByteArray(opt_offsettime) : exif_read_str(offset_tag);
       // string should be +HH:MM or -HH:MM
       static const QRegularExpression re(R"(^([+-])(\d{2}):(\d{2})$)");
       assert(re.isValid());
@@ -723,10 +727,24 @@ ExifFormat::exif_get_exif_time(ExifApp* app)
         int offset_hours = match.captured(1).append(match.captured(2)).toInt();
         int offset_mins = match.captured(1).append(match.captured(3)).toInt();
         res.setOffsetFromUtc(((offset_hours * 60) + offset_mins) * 60);
+      } else if (opt_offsettime) {
+        // Only warn for user supplied offsets.
+        // Offset tags may indicate the offset was unknown, e.g. "   :  ".
+        warning(MYNAME ": OffsetTime is expected to be +HH:MM or -HH:MM, but was %s.\n", time_tag.constData());
       }
     }
+
+    if (subsec_tag) {
+      QByteArray subsec = exif_read_str(subsec_tag);
+      bool ok;
+      double ss = subsec.prepend("0.").toDouble(&ok);
+      if (ok) {
+        res = res.addMSecs(lround(1000.0 * ss));
+      }
+    }
+
   }
-  return res;
+  return res.toUTC();
 }
 
 Waypoint*
@@ -895,7 +913,7 @@ ExifFormat::exif_waypt_from_exif_app(ExifApp* app) const
   gps_datetime = QDateTime(datestamp, timestamp, Qt::UTC);
   if (gps_datetime.isValid()) {
     if (global_opts.debug_level >= 3) {
-      printf(MYNAME "-GPSTimeStamp =   %s\n", qPrintable(gps_datetime.toString(Qt::ISODate)));
+      printf(MYNAME "-GPSTimeStamp =   %s\n", qPrintable(gps_datetime.toString(Qt::ISODateWithMs)));
     }
     wpt->SetCreationTime(gps_datetime);
   } else {
@@ -1555,7 +1573,9 @@ ExifFormat::write()
 
       exif_put_double(GPS_IFD, GPS_IFD_TAG_TIMESTAMP, 0, dt.time().hour());
       exif_put_double(GPS_IFD, GPS_IFD_TAG_TIMESTAMP, 1, dt.time().minute());
-      exif_put_double(GPS_IFD, GPS_IFD_TAG_TIMESTAMP, 2, dt.time().second());
+      exif_put_double(GPS_IFD, GPS_IFD_TAG_TIMESTAMP, 2,
+                      static_cast<double>(dt.time().second()) + 
+                      static_cast<double>(dt.time().msec())/1000.0);
 
       exif_put_str(GPS_IFD, GPS_IFD_TAG_DATESTAMP, CSTR(dt.toString(u"yyyy:MM:dd")));
     } else {
diff --git a/exif.h b/exif.h
index a706021f19fe6ca76e12e71dd5c98b7e3dfe4726..3c0a9f85e3023a089da37e6d64b8bf6932cec5ff 100644 (file)
--- a/exif.h
+++ b/exif.h
@@ -172,7 +172,7 @@ private:
   static void exif_examine_app(ExifApp* app);
   static ExifIfd* exif_find_ifd(ExifApp* app, uint16_t ifd_nr);
   static ExifTag* exif_find_tag(ExifApp* app, uint16_t ifd_nr, uint16_t tag_id);
-  static QDateTime exif_get_exif_time(ExifApp* app);
+  QDateTime exif_get_exif_time(ExifApp* app) const;
   Waypoint* exif_waypt_from_exif_app(ExifApp* app) const;
   static Rational<int> exif_dec2frac(double val, double tolerance);
   ExifTag* exif_put_value(int ifd_nr, uint16_t tag_id, uint16_t type, int count, int index, const void* data) const;
@@ -205,12 +205,14 @@ private:
   char* opt_overwrite{};
   char* opt_frame{};
   char* opt_name{};
+  char* opt_offsettime{};
 
   QVector<arglist_t> exif_args = {
     { "filename", &opt_filename, "Set waypoint name to source filename", "Y", ARGTYPE_BOOL, ARG_NOMINMAX, nullptr },
     { "frame", &opt_frame, "Time-frame (in seconds)", "10", ARGTYPE_INT, "0", nullptr, nullptr },
     { "name", &opt_name, "Locate waypoint for tagging by this name", nullptr, ARGTYPE_STRING, ARG_NOMINMAX, nullptr },
     { "overwrite", &opt_overwrite, "!OVERWRITE! the original file. Default=N", "N", ARGTYPE_BOOL, ARG_NOMINMAX, nullptr },
+    { "offset", &opt_offsettime, "Image Offset Time (+HH:MM or -HH:MM)", nullptr, ARGTYPE_STRING, ARG_NOMINMAX, nullptr },
   };
 };
 #endif // EXIF_H_INCLUDED_
index ab9e479ece8aae5e6282d741ac8977336856db3f..fe814dd920022566483b09c7f97d7ff45e91c395 100644 (file)
@@ -126,6 +126,8 @@ option      exif    name    Locate waypoint for tagging by this name        string                          https://www.
 
 option exif    overwrite       !OVERWRITE! the original file. Default=N        boolean N                       https://www.gpsbabel.org/WEB_DOC_DIR/fmt_exif.html#fmt_exif_o_overwrite
 
+option exif    offset  Image Offset Time (+HH:MM or -HH:MM)    string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_exif.html#fmt_exif_o_offset
+
 file   rwrwrw  shape   shp     ESRI shapefile  shape
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_shape.html
 option shape   name    Source for name field in .dbf   string          0               https://www.gpsbabel.org/WEB_DOC_DIR/fmt_shape.html#fmt_shape_o_name
index 715be12bd7d7d3ff19a70dcc72ea8c2259d559f2..e118164f5586f5ae2552ac86a639c49448751f69 100644 (file)
@@ -71,6 +71,7 @@ File Types (-i and -o options):
          frame                 Time-frame (in seconds)
          name                  Locate waypoint for tagging by this name
          overwrite             (0/1) !OVERWRITE! the original file. Default=N
+         offset                Image Offset Time (+HH:MM or -HH:MM)
        shape                 ESRI shapefile
          name                  Source for name field in .dbf
          url                   Source for URL field in .dbf
index ac71f3259d47caeaead4d82a938ba6e52d714c03..946976c5bac7ceb589df1919c9901fc9588755a7 100644 (file)
@@ -7,10 +7,6 @@
   <userinput>gpsbabel -i gpx -f holiday.gpx -o exif,frame=60 -F IMG0784.JPG</userinput>
 </para>
 <para>
-   If the camera time wasn't adjusted, you should move the track(s) by the this difference.
-   I.e. if the camera time is five minutes behind your time, the track(s) should be shifted
-   five minutes back.
-</para>
-<para>
-  <userinput>gpsbabel -i gpx -f holiday.gpx -x track,move=-5m -o exif,frame=60 -F IMG0784.JPG</userinput>
+   If the camera time wasn't adjusted, you should use the offset option.  You may also need to use the frame option, or 
+   the interpolate filter.
 </para>
diff --git a/xmldoc/formats/options/exif-offset.xml b/xmldoc/formats/options/exif-offset.xml
new file mode 100644 (file)
index 0000000..e76d48b
--- /dev/null
@@ -0,0 +1,10 @@
+<para>
+   Uses the given value instead of the value from the tag OffsetTime, OffsetTimeOriginal or OffsetTimeDigitized.
+   This is useful when the image doesn't contain an OffsetTime* tag and the offset is different from the local time, or when the image contains a tag that is incorrect.
+   The format of this option should match that of the tag OffsetTime*, specifcally it must be "+HH:MM" or "-HH:MM".
+</para>
+<para>
+    If the camera was using China Standard Time, e.g. in the winter in Taiwan, then you should supply
+    an offset of "+8:00".
+<userinput>gpsbabel -i gpx -f holiday.gpx -o exif,offset=+08:00 -F IMG0784.JPG</userinput>
+</para>